#!/usr/bin/env python3
# Copyright 2015 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

import idl_schema
import json_parse
from js_externs_generator import JsExternsGenerator
from datetime import datetime
import model
import sys
import unittest

# The contents of a fake idl file.
fake_idl = """
// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// A totally fake API.
namespace fakeApi {
  enum Greek {
    ALPHA,
    BETA,
    GAMMA,
    DELTA
  };

  dictionary Bar {
    long num;
  };

  dictionary Baz {
    DOMString str;
    long num;
    boolean b;
    Greek letter;
    Greek? optionalLetter;
    long[] arr;
    Bar[]? optionalObjArr;
    Greek[] enumArr;
    any[] anythingGoes;
    Bar obj;
    long? maybe;
    (DOMString or Greek or long[]) choice;
    object plainObj;
    ArrayBuffer arrayBuff;
  };

  dictionary Qux {
    long notOptionalLong;
    long? optionalLong;

    // A map from string to number.
    // <jsexterns>@type {Object<string, number>}</jsexterns>
    object dict;

    static void go();
    static void stop();
  };

  callback VoidCallback = void();

  callback BazGreekCallback = void(Baz baz, Greek greek);

  callback OptionalParamCallback = void(optional Qux qux);

  interface Properties {
    static DOMString lastError();
  };

  interface Functions {
    // Does something exciting! And what's more, this is a multiline function
    // comment! It goes onto multiple lines!
    // |baz| : The baz to use.
    static void doSomething(Baz baz, VoidCallback callback);

    // |callback| : The callback which will most assuredly in all cases be
    // called; that is, of course, iff such a callback was provided and is
    // not at all null.
    [doesNotSupportPromises="Multi-parameter callback"]
    static void bazGreek(optional BazGreekCallback callback);

    [deprecated="Use a new method."] static DOMString returnString();

    static void instanceOfObjectParam([instanceOf=SomeType] object obj);

    static void instanceOfBarObjectParam([instanceOf=Bar] object barObj);

    static void optionalParam(optional OptionalParamCallback callback);

    static void nonFinalOptionalParams(
        DOMString string,
        optional double num,
        object obj,
        [instanceOf = SomeType] object someType,
        [instanceOf = SomeType] optional object optionalSomeType,
        optional boolean bool,
        optional Baz baz,
        double num,
        VoidCallback callback);

    static void multipleOptionalParams(
        optional DOMString param1,
        optional DOMString param2,
        optional object obj,
        [instanceOf = SomeType] optional object optionalSomeType,
        optional VoidCallback callback);
  };

  interface Events {
    // Fired when we realize it's a trap!
    static void onTrapDetected(Baz baz);
  };
};
"""

# The output we expect from our fake idl file.
fake_idl_expected = """// Copyright %s The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// This file was generated by:
//   tools/json_schema_compiler/compiler.py.
// NOTE: The format of types has changed. 'FooType' is now
//   'chrome.fakeApi.FooType'.
// Please run the closure compiler before committing changes.
// See https://chromium.googlesource.com/chromium/src/+/main/docs/closure_compilation.md

/**
 * @fileoverview Externs generated from namespace: fakeApi
 * @externs
 */

/** @const */
chrome.fakeApi = {};

/**
 * @enum {string}
 * @see https://developer.chrome.com/extensions/fakeApi#type-Greek
 */
chrome.fakeApi.Greek = {
  ALPHA: 'ALPHA',
  BETA: 'BETA',
  GAMMA: 'GAMMA',
  DELTA: 'DELTA',
};

/**
 * @typedef {{
 *   num: number
 * }}
 * @see https://developer.chrome.com/extensions/fakeApi#type-Bar
 */
chrome.fakeApi.Bar;

/**
 * @typedef {{
 *   str: string,
 *   num: number,
 *   b: boolean,
 *   letter: !chrome.fakeApi.Greek,
 *   optionalLetter: (!chrome.fakeApi.Greek|undefined),
 *   arr: !Array<number>,
 *   optionalObjArr: (!Array<!chrome.fakeApi.Bar>|undefined),
 *   enumArr: !Array<!chrome.fakeApi.Greek>,
 *   anythingGoes: !Array<*>,
 *   obj: !chrome.fakeApi.Bar,
 *   maybe: (number|undefined),
 *   choice: (string|!chrome.fakeApi.Greek|!Array<number>),
 *   plainObj: Object,
 *   arrayBuff: ArrayBuffer
 * }}
 * @see https://developer.chrome.com/extensions/fakeApi#type-Baz
 */
chrome.fakeApi.Baz;

/**
 * @constructor
 * @private
 * @see https://developer.chrome.com/extensions/fakeApi#type-Qux
 */
chrome.fakeApi.Qux = function() {};

/**
 * @type {number}
 * @see https://developer.chrome.com/extensions/fakeApi#type-notOptionalLong
 */
chrome.fakeApi.Qux.prototype.notOptionalLong;

/**
 * @type {(number|undefined)}
 * @see https://developer.chrome.com/extensions/fakeApi#type-optionalLong
 */
chrome.fakeApi.Qux.prototype.optionalLong;

/**
 * A map from string to number.
 * @type {Object<string, number>}
 * @see https://developer.chrome.com/extensions/fakeApi#type-dict
 */
chrome.fakeApi.Qux.prototype.dict;

/**
 * @see https://developer.chrome.com/extensions/fakeApi#method-go
 */
chrome.fakeApi.Qux.prototype.go = function() {};

/**
 * @see https://developer.chrome.com/extensions/fakeApi#method-stop
 */
chrome.fakeApi.Qux.prototype.stop = function() {};


/**
 * @type {string}
 * @see https://developer.chrome.com/extensions/fakeApi#type-lastError
 */
chrome.fakeApi.lastError;

/**
 * Does something exciting! And what's more, this is a multiline function
 * comment! It goes onto multiple lines!
 * @param {!chrome.fakeApi.Baz} baz The baz to use.
 * @param {function(): void} callback
 * @see https://developer.chrome.com/extensions/fakeApi#method-doSomething
 */
chrome.fakeApi.doSomething = function(baz, callback) {};

/**
 * @param {function(!chrome.fakeApi.Baz, !chrome.fakeApi.Greek): void=} callback
 *     The callback which will most assuredly in all cases be called; that is,
 *     of course, iff such a callback was provided and is not at all null.
 * @see https://developer.chrome.com/extensions/fakeApi#method-bazGreek
 */
chrome.fakeApi.bazGreek = function(callback) {};

/**
 * @return {string}
 * @deprecated Use a new method.
 * @see https://developer.chrome.com/extensions/fakeApi#method-returnString
 */
chrome.fakeApi.returnString = function() {};

/**
 * @param {SomeType} obj
 * @see https://developer.chrome.com/extensions/fakeApi#method-instanceOfObjectParam
 */
chrome.fakeApi.instanceOfObjectParam = function(obj) {};

/**
 * @param {chrome.fakeApi.Bar} barObj
 * @see https://developer.chrome.com/extensions/fakeApi#method-instanceOfBarObjectParam
 */
chrome.fakeApi.instanceOfBarObjectParam = function(barObj) {};

/**
 * @param {function((!chrome.fakeApi.Qux|undefined)): void=} callback
 * @see https://developer.chrome.com/extensions/fakeApi#method-optionalParam
 */
chrome.fakeApi.optionalParam = function(callback) {};

/**
 * @param {string} string
 * @param {?number|undefined} num
 * @param {Object} obj
 * @param {SomeType} someType
 * @param {?SomeType|undefined} optionalSomeType
 * @param {?boolean|undefined} bool
 * @param {?chrome.fakeApi.Baz|undefined} baz
 * @param {number} num
 * @param {function(): void} callback
 * @see https://developer.chrome.com/extensions/fakeApi#method-nonFinalOptionalParams
 */
chrome.fakeApi.nonFinalOptionalParams = function(string, num, obj, someType, optionalSomeType, bool, baz, num, callback) {};

/**
 * @param {string=} param1
 * @param {string=} param2
 * @param {Object=} obj
 * @param {SomeType=} optionalSomeType
 * @param {function(): void=} callback
 * @see https://developer.chrome.com/extensions/fakeApi#method-multipleOptionalParams
 */
chrome.fakeApi.multipleOptionalParams = function(param1, param2, obj, optionalSomeType, callback) {};

/**
 * Fired when we realize it's a trap!
 * @type {!ChromeEvent}
 * @see https://developer.chrome.com/extensions/fakeApi#event-onTrapDetected
 */
chrome.fakeApi.onTrapDetected;""" % datetime.now().year

# A subset of fake_idl. The key difference is that the namespace is private,
# which means that @see links shouldn't be generated.
fake_private_idl = """
// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// A totally fake API.
namespace fakeApiPrivate {
  enum Greek {
    ALPHA,
    BETA,
    GAMMA,
    DELTA
  };

  dictionary Bar {
    long num;
  };

  interface Events {
    // Fired when we realize it's a trap!
    static void onTrapDetected(Baz baz);
  };
};
"""

fake_private_idl_expected = """// Copyright %s The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// This file was generated by:
//   tools/json_schema_compiler/compiler.py.
// NOTE: The format of types has changed. 'FooType' is now
//   'chrome.fakeApiPrivate.FooType'.
// Please run the closure compiler before committing changes.
// See https://chromium.googlesource.com/chromium/src/+/main/docs/closure_compilation.md

/**
 * @fileoverview Externs generated from namespace: fakeApiPrivate
 * @externs
 */

/** @const */
chrome.fakeApiPrivate = {};

/**
 * @enum {string}
 */
chrome.fakeApiPrivate.Greek = {
  ALPHA: 'ALPHA',
  BETA: 'BETA',
  GAMMA: 'GAMMA',
  DELTA: 'DELTA',
};

/**
 * @typedef {{
 *   num: number
 * }}
 */
chrome.fakeApiPrivate.Bar;

/**
 * Fired when we realize it's a trap!
 * @type {!ChromeEvent}
 */
chrome.fakeApiPrivate.onTrapDetected;""" % datetime.now().year

fake_json = """// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

[
  {
    "namespace": "fakeJson",
    "description": "Fake JSON API Stuff",
    "types": [
      {
        "id": "CrazyEnum",
        "type": "string",
        "enum": ["camelCaseEnum", "Non-Characters", "5NumFirst", \
"3Just-plainOld_MEAN"]
      },
      {
        "id": "CrazyObject",
        "type": "object",
        "additionalProperties": {"type": "string"}
      }
    ],
    "properties": {
      "lastError": {
        "type": "string",
        "description": "The lastError."
      }
    },
    "functions": [ {
      "name": "funcWithInlineObj",
      "type": "function",
      "parameters": [
        {
          "type": "object",
          "name": "inlineObj",
          "description": "Evil inline object! With a super duper duper long\
 string description that causes problems!",
          "properties": {
            "foo": {
              "type": "boolean",
              "optional": "true",
              "description": "The foo."
            },
            "bar": {
              "type": "integer",
              "description": "The bar."
            },
            "baz": {
              "type": "object",
              "description": "Inception object.",
              "properties": {
                "depth": {
                  "type": "integer"
                }
              }
            },
            "quu": {
              "type": "binary",
              "description": "The array buffer"
            }
          }
        },
        {
          "name": "callback",
          "type": "function",
          "parameters": [
            {
              "type": "object",
              "name": "returnObj",
              "properties": {
                "str": { "type": "string"}
              }
            }
          ],
          "description": "The callback to this heinous method"
        }
      ],
      "returns": {
        "type": "object",
        "properties": {
          "str": { "type": "string" },
          "int": { "type": "number" }
        }
      }
    },
    {
      "name": "funcWithReturnsAsync",
      "type": "function",
      "parameters": [
        {
          "type": "integer",
          "name": "someNumber",
          "description": "A number parameter"
        }
      ],
      "returns_async": {
        "name": "callback",
        "optional": true,
        "parameters": [
          {
            "name": "anotherNumber",
            "type": "integer",
            "description": "A number that comes back"
          }
        ]
      }
    } ]
  }
]"""

fake_json_expected = """// Copyright %s The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// This file was generated by:
//   tools/json_schema_compiler/compiler.py.
// NOTE: The format of types has changed. 'FooType' is now
//   'chrome.fakeJson.FooType'.
// Please run the closure compiler before committing changes.
// See https://chromium.googlesource.com/chromium/src/+/main/docs/closure_compilation.md

/**
 * @fileoverview Externs generated from namespace: fakeJson
 * @externs
 */

/** @const */
chrome.fakeJson = {};

/**
 * @enum {string}
 * @see https://developer.chrome.com/extensions/fakeJson#type-CrazyEnum
 */
chrome.fakeJson.CrazyEnum = {
  CAMEL_CASE_ENUM: 'camelCaseEnum',
  NON_CHARACTERS: 'Non-Characters',
  _5NUM_FIRST: '5NumFirst',
  _3JUST_PLAIN_OLD_MEAN: '3Just-plainOld_MEAN',
};

/**
 * @typedef {Object}
 * @see https://developer.chrome.com/extensions/fakeJson#type-CrazyObject
 */
chrome.fakeJson.CrazyObject;

/**
 * The lastError.
 * @type {string}
 * @see https://developer.chrome.com/extensions/fakeJson#type-lastError
 */
chrome.fakeJson.lastError;

/**
 * @param {{
 *   foo: (boolean|undefined),
 *   bar: number,
 *   baz: {
 *     depth: number
 *   },
 *   quu: ArrayBuffer
 * }} inlineObj Evil inline object! With a super duper duper long string
 *     description that causes problems!
 * @param {function({
 *   str: string
 * }): void} callback The callback to this heinous method
 * @return {{
 *   str: string,
 *   int: number
 * }}
 * @see https://developer.chrome.com/extensions/fakeJson#method-funcWithInlineObj
 */
chrome.fakeJson.funcWithInlineObj = function(inlineObj, callback) {};

/**
 * @param {number} someNumber A number parameter
 * @param {function(number): void=} callback
 * @see https://developer.chrome.com/extensions/fakeJson#method-funcWithReturnsAsync
 */
chrome.fakeJson.funcWithReturnsAsync = function(someNumber, callback) {};""" % (
    datetime.now().year)


class JsExternGeneratorTest(unittest.TestCase):
  def _GetNamespace(self, fake_content, filename, is_idl):
    """Returns a namespace object for the given content"""
    api_def = (idl_schema.Process(fake_content, filename) if is_idl
        else json_parse.Parse(fake_content))
    m = model.Model()
    return m.AddNamespace(api_def[0], filename)

  def setUp(self):
    self.maxDiff = None # Lets us see the full diff when inequal.

  def testBasic(self):
    namespace = self._GetNamespace(fake_idl, 'fake_api.idl', True)
    self.assertMultiLineEqual(fake_idl_expected,
                              JsExternsGenerator().Generate(namespace).Render())

  def testPrivate(self):
    namespace = self._GetNamespace(fake_private_idl, 'fake_api_private.idl',
                                   True)
    self.assertMultiLineEqual(fake_private_idl_expected,
                              JsExternsGenerator().Generate(namespace).Render())

  def testJsonWithInlineObjectsAndAsyncReturn(self):
    namespace = self._GetNamespace(fake_json, 'fake_api.json', False)
    self.assertMultiLineEqual(fake_json_expected,
                              JsExternsGenerator().Generate(namespace).Render())


if __name__ == '__main__':
  unittest.main()
